iT邦幫忙

2024 iThome 鐵人賽

DAY 14
1
Kubernetes

入門 Kubernetes 到考取 CKA 證照系列 第 14

Day 14 -【Storage】: PV、PVC & StorageClass

  • 分享至 

  • xImage
  •  

Day 14 -【Storage】: PV、PVC & StorageClass

今日目標

  • 了解 PV、PVC、StorageClass 的概念與關係

  • 以實作來了解 reclaim policy 的效果 (Delete、Retain、Recycle)

  • 實際使用 PV、PVC、StorageClass

    • 使用 nfs 作為 storageClass 的 provisioner (optional)

什麼是 PV、PVC、StorageClass

上個章節介紹了 volume,但如果只是單純的使用 volume 作為 Pod 的 storage 存在一些缺點,例如:

  • Volume 的生命週期和 Pod 是一樣的,當 Pod 被刪除時,volume 也會被刪除。

  • 假如定義了 hostPath,當 Pod 轉移到另一個 Node 上時,我們無法保證新的 Node 上也有同樣的「hostPath」,可能造成 Pod 讀不到資料。

另外,Volume 分成好幾種類型,每種的配置方式又有些許不同,效能與適用場景也不同。

例如:volume 類型中,nfs 與 iscsi 乍看之下很類似,但效能與應用場景都不同,在 yaml 上的配置也不同。(延伸閱讀)

開發者在面對五花八門的 volume 寫法時,內心可能會想:「我只是需要一個儲存空間而已阿......」

如果說,當開發者的 Pod 需要儲存空間時,僅需提出需求(例如「要幾G」),剩下直接交給 k8s 管理員來提供實際的 storage,不但能減輕開發者的負擔,也能讓 storage 與 pod 的生命週期解耦。

而這就是 Persistent Volume、Persistent Volume Claim 的概念:

  • Persistent Volume (PV):擁有獨立生命週期的 storage。

  • Persistent Volume Claim (PVC):儲存空間的需求。

K8s 管理員的工作就是當有 PVC 提出後,提供相對應的 PV 給 PVC。

提供 PV 的方式稱為「provisioning」,可以分為以下兩種模式:

簡單講就是手動與自動的差別:

  • Static:由管理員自行定義並建置,例如管理使用 yaml 來建立 PV。

  • Dynamic: 管理員配置 「StorageClass」,後續由 storageClass 來自動建置 PV。

StorageClass 可以讓 PV 的 provisioning 變得更簡單,舉例來說:

  1. 開發者建立了一個 PVC,該 PVC 屬於 storageClass「A」。

  2. StorageClass「A」看到新的 PVC,會自動建立相對應的 PV,而非管理員手動操作。

前面提過,不同的 stroage 方式存在效能、使用場景上的差異,為了解決多種使用需求,一個 cluster 中可以存在多種 storageClass。

了解 PV、PVC、StorageClass 的基本概念後,底下我們先來了解三者的 yaml 寫法,然後再用「生命週期」把概念統整一下。

PV 的 yaml 格式

我們先來看看 PV 的 yaml 格式:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: <pv-name>
spec:
  capacity:
    storage: <storage-size>
  accessModes:
    - <access-mode>
  persistentVolumeReclaimPolicy: <reclaim-policy>
  pv-type:
    <pv-type-configurations>
  • capacity:定義 PV 的大小,例如: 1Gi、500Mi 等。

  • accessModes:定義 PV 的存取模式

    • ReadWriteOnce (RWO):只能被單一 Node 掛載為讀寫模式

    • ReadOnlyMany (ROX):可以被多個 Node 掛載為唯讀模式

    • ReadWriteMany (RWX):可以被多個 Node 掛載為讀寫模式

    • ReadWriteOncePod (RWOP):只能被單一 Pod 掛載為讀寫模式

    被支援的 AccessMode 與下方介紹的 pv-type 有關。初學者先記住 ReadWriteOnce 即可,因為所有的 pv-type 都支援這個模式。

  • persistentVolumeReclaimPolicy:定義當 PVC 被刪除時的行為,有以下三種:

    • Retain:PV 會繼續存在,並保留 PV 裡的資料(真的用不到需自行手動刪除)。雖然 PV 還在,但已經不能再被新的 PVC 使用。

    • Delete: 直接刪除 PV 以及相關的儲存資源(ex. AWS EBS)

    • Recycle:PV 會繼續存在,並刪除 PV 裡的「資料」。之後 PV 可以被新的 PVC 使用。

      Recycle 模式看看就好,因為官網明確表明,建議使用 Dynamic provisioning 的方式來取代 Recycle PV。

  • pv-type: 定義 PV 的類型,例如: nfshostPathlocal 等。其他的 pv-type 可參考官網

    pv-type 支援的 AccessMode 可參考官網

  • pv-type-configurations: 根據不同的 pv-type 而有不同的配置,例如:nfs 需要定義 server、path 等

PVC 的 yaml 格式

我們再來看看 PVC 的 yaml 格式:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
 name: <pvc-name>
spec:
 storageClassName: <storage-class-name>
 accessModes:
   - <access-mode>
 resources:
   requests:
     storage: <request-size>
  • storageClassName:指定 storageClass 來自動建立 PV。若省略此欄位,則使用預設的 storage class,沒有預設的 storage class 就得手動建立 PV。

  • accessModes:同 PV 的 accessModes (RWO、ROX、RWX、RWOP)。

  • resources:定義所需的大小,例如: 1Gi、500Mi 等。

另外,也可以使用 selector 或 volumeName 來指定 PV:

selector:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: <pvc-name>
spec:
  accessModes:
    - <access-mode>
  resources:
    requests:
      storage: <request-size>
  selector: # 使用 selector 來指定 PV
    matchLabels:
      <key>: <value>

volumeName:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: <pvc-name>
spec:
  volumeName: <pv-name> # 使用 volumeName 來指定PV
  accessModes:
    - <access-mode>
  resources:
    requests:
      storage: <request-size>

當建立 PVC 後,K8s 會自動尋找符合條件的 PV,如果符合條件的 PV 不存在,則 PVC 會一直處於 Pending 狀態,直到符合條件的PV被建立。

所謂的「符合條件」規則如下:

  • accessModes 是否符合

  • PV 的 capacity 是否大於或等於 PVC 的 request

  • selector 或 volumeName 是否符合


注意

  • 假設 PV 的 capacity 是 1Gi,PVC 的 request 是 500Mi,若該 PV 與 PVC 綁定了,實際上 PVC 會拿到 1Gi 的空間,而不是 500Mi。

  • PV 與 PVC 是一對一關係,PV 一旦和某個 PVC 綁定後,綁定期間就不能再被其他 PVC 使用。

  • PVC 可以被多個 Pod 同時使用來共享 PV 資源 (除非 AccessMode 是 ReadWriteOncePod)。


StorageClass 的 yaml 格式

storage class 的 yaml 格式如下:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: <storage-class-name>
provisioner: <provisioner>
reclaimPolicy: <reclaim-policy>
allowVolumeExpansion: <true or false>
volumeBindingMode: <volume-binding-mode>
  • provisioner:storageClass 會建立 PV,就需要一個 PV 的「提供者」。可用的 provisioner 清單請見官網

  • reclaimPolicy:同 PV 的 ReclaimPolicy,預設為 Delete

  • allowVolumeExpansion:是否允許 PVC 要求更多的 size,true為允許。

  • volumeBindingMode: 定義 PV 的綁定模式,有以下兩種:

    • Immediate:當 PVC 被建立時,storageClass 會立即建立 PV

    • WaitForFirstConsumer:當 PVC 被建立時,storageClass 不會立即建立 PV,而是等到有 Pod 使用 PVC 時才建立 PV (底下有關 local pv 的例子會使用到)

預設 storage class

Default storage class 用來提供 PV 給沒有指定 storage class 的 PVC。

一個 storage class 是否為預設,取決於 annotations:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: <storage-class-name>
annotations:
  storageclass.kubernetes.io/is-default-class: "true" # 標記為預設
provisioner: <provisioner>
reclaimPolicy: <reclaim-policy>
allowVolumeExpansion: <true or false>
volumeBindingMode: <volume-binding-mode>

若在 yaml 中沒有標記就建立 storage class ,可以用以下方式將 storage class 設定為預設:

kubectl patch storageclass <sc-name> -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

取消預設則是:

kubectl patch storageclass <sc-name> -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'

PV 與 PVC 的生命週期

這裡透過 PV 與 PVC 的生命週期,簡單的梳理一下剛剛看到的各種設定:

Provisioning

此階段為「PV 的產生」,有兩種產生方式:

  • Static:管理員自行定義並建置 PV,例如剛剛看到的 PV yaml。
  • Dynamic: 管理員配置「StorageClass」,後續由 storageClass 來自動建置 PV。

Binding

PVC 被建立後,系統會搜尋可用的 PV 來綁定(binding),情況可分為以下幾種:

  • PVC 如果有設定 storageClassName,建立後系統會找相對應的 storageClass 來產生 PV。

  • PVC 若沒有設定 storageClassName,系統會找 default storageClass 來產生 PV。

  • 若上述兩種情況都沒有符合條件的 storageClass 來產生 PV,則系統依照以下條件搜尋可用的 PV 來綁定:

    • accessModes 是否符合

    • PV 的 capacity 是否大於或等於 PVC 的 request

    • selector 或 volumeName 是否符合

  • 經過上面的搜尋,最終 PVC 會有兩種結果:

    • Bound: 找到合適的 PV 並綁定成功
    • Pending: 找不到合適的 PV

Using

一旦 PVC 與 PV 綁定,Pod 同樣在 yaml 中透過 volume 的寫法掛載 PVC,可以開始使用 PV 的儲存資源。

這時開發人員只需統一撰寫 pod 使用 volume 掛載 PVC 的 yaml 格式 (下面有實作),不像以前一樣因為 volume type 的不同而有不同的寫法。

Reclaiming

當 PVC 被刪除後,PV 的狀態則取決於 reclaimPolicy

ReclaimPolicy PV 的狀態 資料狀態
Retain 繼續留存,但已不可被使用 繼續留存
Delete 直接刪除 直接刪除

以上為 PV & PVC 主要的生命週期階段,接著就來實做看看。

實作:hostPath PV & reclaim policy

Reclaim policy 如果沒有實際測試過效果,乍看之下可能會覺得很抽象。以下我們來實際測試看看,並順便了解 hostPath PV 是如何被 Pod 掛載的。

首先,在 node01 建立兩個目錄:

ssh node01
sudo mkdir -p -m 777 /data/retain /data/delete 

接著建立三個 PV,分別使用不同的 reclaim policy:

# pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-retain
  labels:
    foo: bar
spec:
  capacity:
    storage: 500Mi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: /data/retain
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-delete
  labels:
    foo: bar
spec:
  capacity:
    storage: 500Mi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  hostPath:
    path: /data/delete
kubectl apply -f pv.yaml

接著建立兩個 PVC,分別使用 volumeName 來指定 PV,並將 storageClassName 設為 null,以免系統中存在 default storage class,導致 PVC 綁訂到 dynamic PV:

# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-retain
  labels:
    foo: bar
spec:
  storageClassName: ""
  volumeName: pv-retain
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-delete
  labels:
    foo: bar
spec:
  storageClassName: "" 
  volumeName: pv-delete
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
kubectl apply -f pvc.yaml

檢查三個 PVC 的狀態:

kubectl get pvc -l foo=bar
NAME          STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
pvc-delete    Bound    pv-delete    500Mi      RWO                           <unset>                 52s
pvc-retain    Bound    pv-retain    500Mi      RWO                           <unset>                 52s

從 STATUS 可以看出三個 PVC 皆成功找到了對應的 PV,因此狀態為 Bound。
從 VOLUME 可以看出三個 PVC 皆成功綁定到我們指定的 PV。

然後我們建立第一個 Pod,並使用 pvc-retain:

# retain-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: retain-pod
  labels:
    foo: bar
spec:
  nodeName: node01
  containers:
    - name: busybox
      image: busybox
      command: ["/bin/sh", "-c", "echo 'hello from retain-pod' > /pod-data/retain.txt && sleep 3600"]
      volumeMounts:
        - name: retain-vol
          mountPath: /pod-data
  volumes:
    - name: retain-vol
      persistentVolumeClaim:
        claimName: pvc-retain
kubectl apply -f retain-pod.yaml

yaml 解讀

  • 透過 spec.nodeName 指定 retain-pod 一定要跑在 node01 上,因為只有 node01 上才有 hostPath PV。(nodeName 的用法屬於 scheduling 的範疇,後續會有一個專門的章節來介紹)

  • spec.volumes 中定義了一個叫做 "retain-vol" 的 volume,該 volume 的類別是 persistentVolumeClaim,使用了 pvc-retain 這個 PVC。

  • spec.containers.volumeMounts 中,將 retain-vol 掛載至 retain-pod 的 /pod-data 目錄下。當 retain-pod 啟動時,會在 /pod-data 目錄下建立 retain.txt,內容為 "hello from retain-pod"。

  • 因為 retain-vol 實際上是一個 hostPath PV (pvc-retain -> pv-retain),因此 retain.txt 實際上存在於 node01 的 /data/retain 目錄下。

所以當 pod 被建立起來後,我們能在 node01 的 /data/retain 目錄下看到 retain.txt 的內容:

ssh node01 -- cat /data/retain/retain.txt
hello from retain-pod

確認資料有正確寫入,刪掉 retain-pod & pvc-retain,並觀察 pv-retain 的狀態:

kubectl delete pod retain-pod --force --grace-period=0
kubectl delete pvc pvc-retain
kubectl get pv pv-retain
NAME        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM                STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pv-retain   500Mi      RWO            Retain           Released   default/pvc-retain                  <unset>                          53m

可以看到因為是 Retain 模式,所以 pv-retain 的 STATUS 為 Released,代表原本的 PVC 已被刪除,但 PV 尚未被回收,所以無法再被其他 PVC 綁定使用。

「PV 尚未被收回」可以從 claimRef 中看出來:

kubectl get pv pv-retain -o jsonpath='{.spec.claimRef}'
{"apiVersion":"v1","kind":"PersistentVolumeClaim","name":"pvc-retain","namespace":"default","resourceVersion":"6766874","uid":"e6bd8d92-5f7d-41a3-82d5-def3ed098f21"}

因此將 claimRef 設為 null 就可以手動回收 PV,讓它的 STATUS 變回 Available

kubectl patch pv pv-retain -p '{"spec":{"claimRef": null}}' 
kubectl get pv pv-retain
NAME        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pv-retain   500Mi      RWO            Retain           Available                          <unset>                          4h13m

這時我們重新建一個新的 PVC 與 Pod,嘗試讀取之前留下的 retain.txt:

# new-retain.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: new-pvc
  labels:
    foo: bar
spec:
  storageClassName: ""
  volumeName: pv-retain
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
---
apiVersion: v1
kind: Pod
metadata:
  name: new-pod
  labels:
    foo: bar
spec:
  nodeName: node01
  containers:
    - name: busybox
      image: busybox
      command: ["/bin/sh", "-c", "cat /pod-data/retain.txt; sleep 3600"]
      volumeMounts:
        - name: retain-vol
          mountPath: /pod-data
  volumes:
    - name: retain-vol
      persistentVolumeClaim:
        claimName: new-pvc-retain
kubectl apply -f new-retain.yaml

等 new-pod 跑起來後,檢查它的 log 即可查看 "cat /pod-data/retain.txt" 的輸出結果:

kubectl logs new-pod
hello from retain-pod

因為 reclaim policy 為 retain,舊的 PVC 即使被刪除,資料還是會繼續留存:

ssh node01 -- cat /data/retain/retain.txt
hello from retain-pod

OK,測試完 retain,我們來看看 delete。

現在建立一個新的 Pod,掛載 pvc-delete 並寫入資料:

# foo.yaml
apiVersion: v1
kind: Pod
metadata:
  name: foo
  labels:
    foo: bar
spec:
  nodeName: node01
  containers:
    - name: busybox
      image: busybox
      command: ["/bin/sh", "-c", "echo 'foo data for delete' > /delete-data/delete.txt && echo 'foo data for recycle' > /recycle-data/recycle.txt && sleep 3600"]
      volumeMounts:
        - name: delete-vol
          mountPath: /delete-data
  volumes:
    - name: recycle-vol
      persistentVolumeClaim:
        claimName: pvc-recycle
kubectl apply -f foo.yaml

查看資料是否成功寫入:

ssh node01 -- cat /data/delete/delete.txt 
foo data for delete

現在我們刪掉 foo pod、pvc-delete 與 pvc-recycle,並檢查 PV 的狀態:

如果不刪掉 foo pod,PVC 一直卡在 Terminating 且刪不掉,因為 foo pod 仍在使用這兩個 pvc,導致觸發 finalizer 的機制。

kubectl delete pod foo --grace-period=0 --force
kubectl delete pvc pvc-delete pvc-recycle
kubectl get pv pv-delete pv-recycle
NAME         CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pv-recycle   500Mi      RWO            Recycle          Available                          <unset>                          6h
Error from server (NotFound): persistentvolumes "pv-delete" not found

正如上面介紹的那樣,當 reclaim policy 為 Recycle 時,刪除 PVC 之後 PV 會重新變回 Available 狀態,而 policy 為 Delete 時則直接刪除 PV。

hostPath 的缺點

在上面的範例中我們使用 nodeName 指定了 Pod 的去向,但在預設情況下,Pod 的去向是由 schduler 來決定的,這樣就會衍伸出一些問題:

  1. 在讀取資料的場景中,除非我們能保證所有 node 上都有某個 hostPath,且 hostPath 的資料每台 node 都一致,否則當 Pod 很可能會因為所在的 node 上沒有對應的 hostPath 或資料而造成資料讀取上的錯誤。

  2. 如果 Pod 在工作時對 hostPath 寫入了一些資料,但 Pod 有可能因為某些原因而重新啟動,重啟時被調度到其他 node 上,那之前寫入的舊資料就讀不到了。

當然,我們可以使用 nodeName 或 nodeSelector 來指定 Pod 要跑在哪個 Node 上,但這也無法避免上述提到的第二個問題,且當面對一個複雜的 cluster、有一堆 Pod、一堆 hostPath 時,逐一在每個 Pod 撰寫 scheduling 的相關配置相當麻煩,也降低了調度上的靈活度。(註:有關 nodeName、nodeSelector 的使用方式會在後續 scheduling 的章節介紹)

因此,hostPath 常見的使用環境為「single-node cluster」,這在官網上也有明確的說明:

hostPath - HostPath volume (for single node testing only; WILL NOT WORK in a multi-node cluster; consider using local volume instead)

正如上方所引用的,官方建議使用「local」類型的 PV 來取代 hostPath PV,底下我們就來實作看看。

實作:local Persistent Volume

在編寫 local 類型的 PV 會掛載「特定 Node」上的儲存空間,例如硬碟、分割槽、目錄,聽起來似乎與 hostPath 很像,但差別就在於:

當 scheduler 分配 Pod 時,不會考慮是否該 node 上有存在 hostPath PV,不過會嘗試將 Pod 分配到那些有 local PV 的 node 上。

舉例來說,同樣都是想讓 Pod 正確掛載 node01 上的 /data 目錄,scheduler 在分配 Pod 時會將「該 node 是否有掛載 /data 的 local PV」納入 scheduling 的考量中。

反之「該 node 上是否有掛載 /data 的 hostPath PV」則不在考量範圍內。

為什麼 scheduler 會「特別考慮」local PV 的需求呢?原因在於編寫 local PV 的 yaml 時,必須設定 node affinity來指定 PV 要建立在哪些 node 上。(node affinity 也是 scheduling 的一種方式,之後會再介紹)

另外在實作時,建議搭配 storageClass 的 waitForFirstConsumer 效果來延遲 PV 與 PVC 的綁定時間,原因如下:

在沒有 storageClass 的情況下,我們來假設一個場景:

  • 假設有 3 個 local 類型的 local PV,掛載的 local path 如下:

    node01 node02 node03
    local-pv-1 (/data) local-pv-2 (/dev/sda) local-pv-3 (/data)
    CPU GPU GPU
  • 此時我們建立一個 data-pvc,有兩個 PV (local-pv-1 與 local-pv-3)符合 data-pvc 的需求,系統就會自動將 data-pvc 與其中一個 local PV 綁定:

    node01 node02 node03
    local-pv-1 (/data) local-pv-2 (/dev/sda) local-pv-3 (/data)
    data-pvc - -
    CPU GPU GPU
  • 接著,有一個 new-pod 想要取得 /data 上的資料,因此使用了 new-pvc。但開發者出於其他需求,在 pod yaml 中設定了 schduling 規則,告訴 schduler 把這個 pod 只能被放到 node02 或是 node03 上。

  • 但因為 new-pvc 已經先和 node01 的 local-pv-1 綁定了,而 pod 又不能去 node01 而造成衝突,因此 new-pod 會無法成功跑起來。

    node01 node02 node03
    local-pv-1 (/data) local-pv-2 (/dev/sda) local-pv-3 (/data)
    data-pvc - -
    - new-pod (x) new-pod (x)
    CPU GPU GPU

因此,我們使用 storageClass 的 waitForFirstConsumer 來延遲 PV 與 PVC 的綁定時間。以上面的情境來說,當 data-pvc 被建立時並不會馬上綁定 PV,而是與 new-pod (consumer) 一起被 scheduler 放到最合適的 node 上:

node01 node02 node03
local-pv-1 (/data) local-pv-2 (/dev/sda) local-pv-3 (/data)
- - data-pvc
- - new-pod
CPU GPU GPU

總之,waitForFirstConsumer 的作用就是讓 PV 與 PVC 的綁定時間延遲,讓 scheduler 能綜合考量多個因素,將 PVC 與 Pod 放到最合適的 node 上。


底下直接來看一個實作:

我們想確保 Pod 一定能存取到 node01 上的 /data/local 目錄,以下依照需求來建立 local PV:

  • 在 node01 上建立 /data/local 目錄,並在裡面創建一個檔案:

    ssh node01
    sudo mkdir -p /data/local
    echo "testing local pv" > /data/local/test.txt
    
  • 返回 Master Node 上,建立 storage class:

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: local-storage
    provisioner: kubernetes.io/no-provisioner
    volumeBindingMode: WaitForFirstConsumer
    
    kubectl apply -f storage-class.yaml
    

    這個 storage class 並非用來做 dynamic provisioning,而是用來延遲 PV 與 PVC 的綁定時間。

  • 建立 local PV:

    # local-pv.yaml
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: local-pv
    spec:
      capacity:
        storage: 10Gi
      volumeMode: Filesystem
      accessModes:
      - ReadWriteOnce
      persistentVolumeReclaimPolicy: Delete
      storageClassName: local-storage
      local:
        path: /data/local
      nodeAffinity: # 指定 PV 要建立在哪個 Node 上
        required:
          nodeSelectorTerms:
          - matchExpressions: # 使用 Node 的 Label 篩選
            - key: kubernetes.io/hostname
              operator: In
              values:
              - node01
    
    kubectl apply -f local-pv.yaml
    
  • 檢查 PV 的狀態:

    kubectl get pv local-pv
    
    NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS    VOLUMEATTRIBUTESCLASS   REASON   AGE
    local-pv   10Gi       RWO            Delete           Available           local-storage   <unset>                          3s
    
  • 建立 PVC:

    # local-pvc.yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: local-pvc
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 5Gi
      storageClassName: local-storage
    
    kubectl apply -f local-pvc.yaml
    
  • 檢查 PVC 的狀態:

    kubectl get pvc local-pvc
    
    NAME        STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS    VOLUMEATTRIBUTESCLASS   AGE
    local-pvc   Pending                                      local-storage   <unset>                 2s
    
    

    因為還沒有任何 Pod 使用 PVC,所以 PVC 的狀態是 Pending,已經達到了延遲綁定的目的。

  • 建立 Pod:

    # nginx-local.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx-local
    spec:
      volumes:
      - name: local-vol
        persistentVolumeClaim:
          claimName: local-pvc
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
        volumeMounts:
        - mountPath: /usr/share/nginx/html
          name: local-vol
    
    kubectl apply -f nginx-local.yaml
    

    等 Pod 跑起來後再次檢查 PVC 的狀態,就會發現已經被綁定了。

  • 檢查 Pod 是否掛載成功:

    kubectl exec -it nginx-local -- cat /usr/share/nginx/html/test.txt
    
    testing local pv
    

以上為 local PV 的使用方式,能夠確保需要特定資料的 Pod 一定會被分配到有 local PV 的 Node 上,解決了 hostPath 的問題。

對於開發者來說,local PV 可以讓他們在撰寫 yaml 時不用特別為「底層」 storage 的問題寫 scheduling 規則,真正的讓底層 storage 與 pod 的設定解耦。

NFS StorageClass (Optional)

以下實作如果在 killercoda 上執行,可能會遇到一些權限問題,建議在自己的虛擬機上操錯。

前面雖然建立了一個 local-storage 的 storageClass,但由於使用「kubernetes.io/no-provisioner」作為 provisioner,因此只能達到「分類」、延遲綁定的效果,它只是一個「邏輯上」的 storageClass,並不能像前面介紹的那樣自動建立 PV。

因此這裡我們來實際建立一個功能完整的 storageClass:使用 nfs 作為 storageClass 的 provisioner。礙於篇幅,實作部分有興趣的話請參考附錄

今日小結

今天我們介紹了 PV、PVC 和 storageClass 的使用,PV 與 PVC 能把儲存空間從 Pod 獨立出來,在資料可以持續保存的情況下,讓 Pod 能夠更加靈活的使用儲存空間。而 storageClass 則能會自動管理 PV 的建立與刪除,讓我們只需建立 PVC,就能拿到儲存空間與達到「分類」管理的效果。

今天就是「Storage」章節的最後一篇,明天我們將進入「Workloads & Scheduling」章節,來談談該如何調度 Pod 以及資源管理。


參考資料

Persistent Volumes

Configure a Pod to Use a PersistentVolume for Storage

Volumes---local

How To Set Up an NFS Mount on Ubuntu 20.04

Kubernetes NFS Subdir External Provisioner

How to Setup Dynamic NFS Provisioning in a Kubernetes Cluster

透過 NFS Server 在 K3s cluster 新增 Storage Class

解决nfs-common.service is masked 问题


上一篇
Day 13 -【Storage】:Volume 的三種基本應用 --- emptyDir、hostPath、configMap & secret
下一篇
Day 15 -【Workloads & Scheduling】:Manual Scheduling(上):nodeName & nodeSelector
系列文
入門 Kubernetes 到考取 CKA 證照32
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言